class Element {
    static SelectById(id) {
        return document.querySelector(id);
    }

    static SetSize(id, width, height) {
        const element = this.SelectById(id);
        element.style.width = width + "px";
        element.style.height = height + "px";
    }

    static SetSizeByElement(templateId, targetId) {
        const size = this.SelectById(templateId).getBoundingClientRect();
        Element.SetSize(targetId, size.width, size.height);
    }

    static SetCanvasSize(id, width, height) {
        const element = Element.SelectById(id);
        element.width = width;
        element.height = height;
    }

    static SetCanvasSizeByElement(templateId, targetId) {
        const size = this.SelectById(templateId).getBoundingClientRect();
        this.SetCanvasSize(targetId, size.width, size.height);
    }

}

class Conversion {
    static DegToRad(deg) {
        return deg * Math.PI / 180;
    }

    static RadToDeg(rad) {
        return rad * 180 / Math.PI;
    }
}

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    distance(point) {
        const dx = this.x - point.x;
        const dy = this.y - point.y;
        return Math.hypot(dx, dy);
    }

    pointOnCircle(radius, degree) {
        const angle = Conversion.DegToRad(degree);
        const dx = this.x + radius * Math.cos(angle);
        const dy = this.y + radius * Math.sin(angle);
        return new Point(dx, dy);
    }

    add(point) {
        return new Point(this.x + point.x, this.y + point.y);
    }

    multiNumber(number) {
        const dx = number * this.x;
        const dy = number * this.y;
        return new Point(dx, dy);
    }

    threePointsAngle(vertexB, vertexC) {
        const vertexA = new Point(this.x, this.y);
        const a = vertexB.distance(vertexC);
        const b = vertexA.distance(vertexC);
        const c = vertexA.distance(vertexB);
        const cosA = (b * b + c * c - a * a) / (2 * b * c);
        return Conversion.RadToDeg(Math.acos(cosA));
    }

    static PolylineDistance(points) {
        let result = 0;
        if (points.length < 2) return null;
        for (let i = 1; i < points.length; i++) {
            result += points[i - 1].distance(points[i]);
        }
        return result;
    }

    static Sum(points) {
        let dx = 0, dy = 0;
        for (let i = 0; i < points.length; i++) {
            dx += points[i].x;
            dy += points[i].y;
        }
        return new Point(dx, dy);
    }

    static GetCursorPosition(id, event) {
        const rect = Element.SelectById(id).getBoundingClientRect();
        const dx = event.clientX - rect.left;
        const dy = event.clientY - rect.top;
        return new Point(dx, dy);
    }

    /***
     * 计算贝塞尔曲线上一定数量的点
     * @param points {[Point]} 控制点组成的点组。
     * @param count {number} 需要计算多少点。
     * @return {null|*[Point]}
     * @constructor
     */
    static Bezier(points, count) {
        let length = points.length, result = [], yangHui = [];
        count = count - 1;
        if (length < 2) return null;
        // 解杨辉三角
        yangHui[0] = yangHui[1] = 1;
        for (let i = 3; i <= length; i++) {
            let temp = [];
            for (let j = 0; j < i - 1; j++) {
                temp[j] = yangHui[j];
            }
            yangHui[0] = yangHui[i - 1] = 1;
            for (let j = 0; j < i - 2; j++) {
                yangHui[j + 1] = temp[j] + temp[j + 1];
            }
        }
        // 计算曲线上的一定数量的点
        for (let i = 0; i < count; i++) {
            let time = i / count;
            let point = new Point(0, 0);
            for (let j = 0; j < length; j++) {
                const constant = Math.pow(1 - time, length - j - 1) *
                    Math.pow(time, j) * yangHui[j];
                const p = points[j].multiNumber(constant);
                point = point.add(p);
            }
            result.push(point);
        }
        result.push(points[length - 1]);
        return result;
    }
}

class Canvas {
    constructor(canvasId) {
        this.id = canvasId;
        this.canvas = Element.SelectById(canvasId);
        this.width = this.canvas.width;
        this.height = this.canvas.height;
        this.context = this.canvas.getContext("2d");
        this.center = new Point(this.width / 2, this.height / 2);
    }

    setSize(width, height) {
        this.canvas.width = width;
        this.canvas.height = height;
        this.width = this.canvas.width;
        this.height = this.canvas.height;
        this.center = new Point(this.width / 2, this.height / 2);
    }

    setSizeFrom(id) {
        const size = document.querySelector(id).getBoundingClientRect();
        this.setSize(size.width, size.height);
    }

    setColor(fill, stroke) {
        this.context.save();
        this.context.fillStyle = fill;
        this.context.strokeStyle = stroke;
        this.context.beginPath();
    }

    setDrawType(type) {
        switch (type) {
            case "fill":
                this.context.fill();
                break;
            case "stroke":
                this.context.stroke();
                break;
            case "both":
                this.context.fill();
                this.context.stroke();
                break;
        }
        this.context.restore();
    }

    setLineStyle(cap, join, width) {
        const capArray = ["butt", "round", "square"];
        const joinArray = ["bevel", "round", "miter"];
        this.context.lineCap = cap >= 0 && cap < 3 ? capArray[cap] : 0;
        this.context.lineJoin = join >= 0 && join < 3 ? joinArray[join] : 0;
        this.context.lineWidth = width;
    }

    Line(start, end, color, cap, join, width) {
        this.setColor(color, color);
        this.setLineStyle(cap, join, width);
        this.context.moveTo(start.x, start.y);
        this.context.lineTo(end.x, end.y);
        this.setDrawType("stroke");
    }

    Polyline(points, color, cap, join, width, type) {
        this.setColor(color, color);
        this.setLineStyle(cap, join, width);
        this.context.moveTo(points[0].x, points[0].y);
        for (let i = 1; i < points.length; i++) {
            this.context.lineTo(points[i].x, points[i].y);
        }
        this.setDrawType(type);
    }

    Circle(center, radius, fillColor, strokeColor, type) {
        this.setColor(fillColor, strokeColor);
        this.context.arc(center.x, center.y, radius, 0, 2 * Math.PI);
        this.setDrawType(type);
    }

    Circles(centers, radius, fillColor, strokeColor, type) {
        for (let i = 0; i < centers.length; i++) {
            this.Circle(centers[i], radius, fillColor, strokeColor, type)
        }
    }

    /***
     * 设置绘制曲线的点的数量。
     * @param controlPoints {{Point}}
     * @return {number}
     */
    setBezierPointsCount(controlPoints) {
        const polyDistance = Point.PolylineDistance(controlPoints);
        const count = Math.ceil(polyDistance / 10);
        return count;
    }

    /***
     * 绘制贝塞尔样条。
     * @param controlPoints {[Point]} 控制点点组。
     * @param pointsCount {number} 组成曲线的点的数量，此曲线本质上由线段组成，点越多月光滑。
     * @param color {string}
     * @param cap {number}
     * @param join {number}
     * @param width {number}
     * @param type {string}
     * @constructor
     */
    Bezier(controlPoints, color, cap, join, width, type) {
        const count = this.setBezierPointsCount(controlPoints);
        const bz = Point.Bezier(controlPoints, count);
        this.Polyline(bz, color, cap, join, width, type);
    }

    BezierByTime(controlPoints, time, color, cap, join, width, type) {
        const count = this.setBezierPointsCount(controlPoints);
        const bz = Point.Bezier(controlPoints, count);
        if (time < 0 || time > 100) return null;
        const end = Math.ceil(time / 100 * count);
        const temp = bz.slice(0, end);
        this.Polyline(temp, color, cap, join, width, type);
        return temp[temp.length - 1];
    }

    drawCanvas(canvas) {
        this.context.drawImage(canvas.canvas, 0, 0);
    }

    Clear() {
        this.canvas.width = this.width;
    }

    saveAsPng(name) {
        const dataURL = this.canvas.toDataURL('image/png');
        let download = document.createElement('a');
        download.download = name || '未命名';
        download.href = dataURL;
        let event = new MouseEvent("click")
        download.dispatchEvent(event);
    }

    static randomColor(transparency) {
        let array = [];
        for (let i = 0; i < 3; i++) {
            array.push(Math.floor(Math.random() * 256));
        }
        return "rgba(" + array[0] + "," + array[1] + "," + array[2] + "," + transparency + ")";
    }
}

const cvs = new Canvas("#canvas");
cvs.setSizeFrom("#screen");
const temp = new Canvas("#temp");
temp.setSizeFrom("#screen");

function BezierAnime(points,color, time) {
    cvs.BezierByTime(points, time, color, 1, 1, 5, "stroke");
}


function controlAnime(points, color, time) {
    if (points.length < 1) return null;
    let pointsArray = [];
    for (let i = 1; i < points.length; i++) {
        const p1 = points[i - 1];
        const p2 = points[i];
        const distance = p1.distance(p2);
        const percent = time / 100 * distance;
        const angle = p2.y > p1.y ? p1.threePointsAngle(new Point(p1.x + 10, p1.y), p2) :
            -p1.threePointsAngle(new Point(p1.x + 10, p1.y), p2);
        const temp = p1.pointOnCircle(percent, angle);
        cvs.Line(p1, temp, color, 1, 1, 3);
        pointsArray.push(temp);
    }
    return pointsArray;
}

function childrenBAnime(points, color, time){
    const length = points.length;
    if (length < 2) return null;
    for (let i = 2; i < points.length; i++) {
        const array = [points[i-2],points[i-1],points[i]];
        BezierAnime(array,color, time);
    }
}

let controlColor = [];

function drawEachControl(controlPoints, time) {
    let temp = controlPoints;
    BezierAnime(temp, "red",time);
    for (let i = 1; i < controlPoints.length; i++) {
        if (controlColor.length < controlPoints.length - 1) {
            controlColor.push(Canvas.randomColor(0.5));
        }
        childrenBAnime(temp, controlColor[i-1], time);
        temp = controlAnime(temp, controlColor[i - 1], time);
        cvs.Polyline(temp, "rgba(30,30,30,0.2)", 2, 0, 4, "stroke");
        cvs.Circles(temp, 5, "white", "black", "both");
    }
    cvs.Circles(controlPoints, 5, "red", "black", "both");
}

let mousedown = 0;
let controlPoints = [];

let movieState = 0; // 0:未开始 1:播放中 2:播放结束
let bTime = 0;


let start;

function step(timestamp) {
    if (start === undefined)
        start = timestamp;
    const time = Math.max(0, 10 - (timestamp - start));

    if (time === 0) {
        start = timestamp;

        const count = cvs.setBezierPointsCount(controlPoints);
        const points = Point.Bezier(controlPoints, count);

        if (movieState === 1) {
            if (bTime < 100) {
                cvs.Clear();
                //anime(controlPoints, controlPoints.length * 100, bTime);
                bTime += 0.5;
                cvs.Polyline(controlPoints, "rgba(30,30,30,0.2)", 2, 0, 4, "stroke");
                //BezierAnime(controlPoints, bTime);
                drawEachControl(controlPoints, bTime);
            } else {
                cvs.Clear();
                cvs.Polyline(controlPoints, "rgba(30,30,30,0.2)", 2, 0, 4, "stroke");
                BezierAnime(controlPoints,"red", bTime);
                cvs.Circles(controlPoints, 5, "red", "black", "both");
                bTime = 0;
                movieState = 2;
            }
        }
    }
    // if (elapsed < 3000) { // 在两秒后停止动画

    window.requestAnimationFrame(step);
    // }
}


cvs.canvas.addEventListener("click", (e) => {
    const cursor = Point.GetCursorPosition("#canvas", e);
    const length = controlPoints.length;
    if (movieState === 0) {
        controlPoints.push(cursor);
        cvs.Circle(cursor, 5, "red", "black", "both");
        if (length > 0) {
            cvs.Clear();
            cvs.Polyline(controlPoints, "rgba(30,30,30,0.2)", 2, 0, 4, "stroke");
            BezierAnime(controlPoints, "red",100);
            cvs.Bezier(controlPoints, "red", 1, 1, 1, "stroke");
            cvs.Circles(controlPoints, 5, "red", "black", "both");
        }
    } else if (movieState === 2) {
        cvs.Clear();
        controlPoints = [];
        controlPoints.push(cursor);
        cvs.Circle(cursor, 5, "red", "black", "both");
        movieState = 0;
    }
});


document.querySelector("#play").addEventListener("click", (e) => {
    const length = controlPoints.length;
    if (length > 1 && movieState !== 1) {
        movieState = 1;
        window.requestAnimationFrame(step);
    }
});
